diff options
Diffstat (limited to 'app/api/data-room/[projectId]/download-multiple/route.ts')
| -rw-r--r-- | app/api/data-room/[projectId]/download-multiple/route.ts | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/app/api/data-room/[projectId]/download-multiple/route.ts b/app/api/data-room/[projectId]/download-multiple/route.ts new file mode 100644 index 00000000..64c87b55 --- /dev/null +++ b/app/api/data-room/[projectId]/download-multiple/route.ts @@ -0,0 +1,162 @@ +// app/api/data-room/[projectId]/download-multiple/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { FileService, type FileAccessContext } from '@/lib/services/fileService'; +import { promises as fs } from 'fs'; +import path from 'path'; +import archiver from 'archiver'; +import { Readable } from 'stream'; +import db from "@/db/db"; +import { fileItems } from "@/db/schema/fileSystem"; +import { eq, inArray } from "drizzle-orm"; + +export async function POST( + request: NextRequest, + { params }: { params: { projectId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const body = await request.json(); + const { fileIds } = body; + + if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) { + return NextResponse.json( + { error: '파일 ID가 제공되지 않았습니다' }, + { status: 400 } + ); + } + + // 너무 많은 파일 방지 (최대 100개) + if (fileIds.length > 100) { + return NextResponse.json( + { error: '한 번에 최대 100개의 파일만 다운로드할 수 있습니다' }, + { status: 400 } + ); + } + + const context: FileAccessContext = { + userId: Number(session.user.id), + userDomain: session.user.domain || 'partners', + userEmail: session.user.email, + ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, + userAgent: request.headers.get('user-agent') || undefined, + }; + + const fileService = new FileService(); + const downloadableFiles: Array<{ + file: any; + absolutePath: string; + }> = []; + + // 각 파일의 접근 권한 확인 및 경로 확인 + for (const fileId of fileIds) { + // 권한 확인 + const hasAccess = await fileService.checkFileAccess( + fileId, + context, + 'download' + ); + + if (!hasAccess) { + console.warn(`파일 ${fileId}에 대한 다운로드 권한이 없습니다`); + continue; + } + + // 파일 정보 가져오기 + const file = await db.query.fileItems.findFirst({ + where: eq(fileItems.id, fileId), + }); + + if (!file || !file.filePath || file.type !== 'file') { + console.warn(`파일 ${fileId}를 찾을 수 없거나 폴더입니다`); + continue; + } + + // 실제 파일 경로 구성 + const nasPath = process.env.NAS_PATH || "/evcp_nas"; + const isProduction = process.env.NODE_ENV === "production"; + + let absolutePath: string; + if (isProduction) { + const relativePath = file.filePath.replace('/api/files/', ''); + absolutePath = path.join(nasPath, relativePath); + } else { + absolutePath = path.join(process.cwd(), 'public', file.filePath); + } + + // 파일 존재 여부 확인 + try { + await fs.access(absolutePath); + downloadableFiles.push({ file, absolutePath }); + + // 다운로드 카운트 증가 및 로그 기록 + await fileService.downloadFile(fileId, context); + } catch (error) { + console.warn(`파일 ${absolutePath}를 찾을 수 없습니다`); + } + } + + if (downloadableFiles.length === 0) { + return NextResponse.json( + { error: '다운로드 가능한 파일이 없습니다' }, + { status: 404 } + ); + } + + // ZIP 스트림 생성 + const archive = archiver('zip', { + zlib: { level: 5 } // 압축 레벨 (1-9, 5가 균형적) + }); + + // 스트림을 Response로 변환 + const stream = new ReadableStream({ + start(controller) { + archive.on('data', (chunk) => controller.enqueue(chunk)); + archive.on('end', () => controller.close()); + archive.on('error', (err) => controller.error(err)); + }, + }); + + // 파일들을 ZIP에 추가 + for (const { file, absolutePath } of downloadableFiles) { + try { + const fileBuffer = await fs.readFile(absolutePath); + + // 파일명 중복 방지를 위한 고유 이름 생성 + const uniqueName = `${path.parse(file.name).name}_${file.id.slice(0, 8)}${path.extname(file.name)}`; + + archive.append(fileBuffer, { name: uniqueName }); + } catch (error) { + console.error(`파일 추가 실패: ${file.name}`, error); + } + } + + // ZIP 완료 + archive.finalize(); + + // Response Headers 설정 + const headers = new Headers(); + headers.set('Content-Type', 'application/zip'); + headers.set('Content-Disposition', 'attachment; filename="files.zip"'); + headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + headers.set('Pragma', 'no-cache'); + headers.set('Expires', '0'); + + return new NextResponse(stream, { + status: 200, + headers, + }); + + } catch (error) { + console.error('다중 파일 다운로드 오류:', error); + return NextResponse.json( + { error: '다중 파일 다운로드에 실패했습니다' }, + { status: 500 } + ); + } +} |
